3 * Internationalisation code.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * @defgroup Language Language
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
34 global $wgLanguageNames;
35 require_once( dirname( __FILE__
) . '/Names.php' );
37 if ( function_exists( 'mb_strtoupper' ) ) {
38 mb_internal_encoding( 'UTF-8' );
42 * a fake language converter
52 function __construct( $langobj ) { $this->mLang
= $langobj; }
53 function autoConvertToAllVariants( $text ) { return array( $this->mLang
->getCode() => $text ); }
54 function convert( $t ) { return $t; }
55 function convertTo( $text, $variant ) { return $text; }
56 function convertTitle( $t ) { return $t->getPrefixedText(); }
57 function getVariants() { return array( $this->mLang
->getCode() ); }
58 function getPreferredVariant() { return $this->mLang
->getCode(); }
59 function getDefaultVariant() { return $this->mLang
->getCode(); }
60 function getURLVariant() { return ''; }
61 function getConvRuleTitle() { return false; }
62 function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
63 function getExtraHashOptions() { return ''; }
64 function getParsedTitle() { return ''; }
65 function markNoConversion( $text, $noParse = false ) { return $text; }
66 function convertCategoryKey( $key ) { return $key; }
67 function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
68 function armourMath( $text ) { return $text; }
72 * Internationalisation code
78 * @var LanguageConverter
82 var $mVariants, $mCode, $mLoaded = false;
83 var $mMagicExtensions = array(), $mMagicHookDone = false;
84 private $mHtmlCode = null;
86 var $dateFormatStrings = array();
87 var $mExtendedSpecialPageAliases;
89 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
92 * ReplacementArray object caches
94 var $transformData = array();
97 * @var LocalisationCache
99 static public $dataCache;
101 static public $mLangObjCache = array();
103 static public $mWeekdayMsgs = array(
104 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
108 static public $mWeekdayAbbrevMsgs = array(
109 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
112 static public $mMonthMsgs = array(
113 'january', 'february', 'march', 'april', 'may_long', 'june',
114 'july', 'august', 'september', 'october', 'november',
117 static public $mMonthGenMsgs = array(
118 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
119 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
122 static public $mMonthAbbrevMsgs = array(
123 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
124 'sep', 'oct', 'nov', 'dec'
127 static public $mIranianCalendarMonthMsgs = array(
128 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
129 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
130 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
131 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
134 static public $mHebrewCalendarMonthMsgs = array(
135 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
136 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
137 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
138 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
139 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
142 static public $mHebrewCalendarMonthGenMsgs = array(
143 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
144 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
145 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
146 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
147 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
150 static public $mHijriCalendarMonthMsgs = array(
151 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
152 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
153 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
154 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
161 static public $durationIntervals = array(
162 'millennia' => 31557600000,
163 'centuries' => 3155760000,
164 'decades' => 315576000,
165 'years' => 31557600, // 86400 * 365.25
174 * Get a cached language object for a given language code
175 * @param $code String
178 static function factory( $code ) {
179 if ( !isset( self
::$mLangObjCache[$code] ) ) {
180 if ( count( self
::$mLangObjCache ) > 10 ) {
181 // Don't keep a billion objects around, that's stupid.
182 self
::$mLangObjCache = array();
184 self
::$mLangObjCache[$code] = self
::newFromCode( $code );
186 return self
::$mLangObjCache[$code];
190 * Create a language object for a given language code
191 * @param $code String
192 * @throws MWException
195 protected static function newFromCode( $code ) {
196 // Protect against path traversal below
197 if ( !Language
::isValidCode( $code )
198 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
200 throw new MWException( "Invalid language code \"$code\"" );
203 if ( !Language
::isValidBuiltInCode( $code ) ) {
204 // It's not possible to customise this code with class files, so
205 // just return a Language object. This is to support uselang= hacks.
206 $lang = new Language
;
207 $lang->setCode( $code );
211 // Check if there is a language class for the code
212 $class = self
::classFromCode( $code );
213 self
::preloadLanguageClass( $class );
214 if ( MWInit
::classExists( $class ) ) {
219 // Keep trying the fallback list until we find an existing class
220 $fallbacks = Language
::getFallbacksFor( $code );
221 foreach ( $fallbacks as $fallbackCode ) {
222 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
223 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
226 $class = self
::classFromCode( $fallbackCode );
227 self
::preloadLanguageClass( $class );
228 if ( MWInit
::classExists( $class ) ) {
229 $lang = Language
::newFromCode( $fallbackCode );
230 $lang->setCode( $code );
235 throw new MWException( "Invalid fallback sequence for language '$code'" );
239 * Returns true if a language code string is of a valid form, whether or
240 * not it exists. This includes codes which are used solely for
241 * customisation via the MediaWiki namespace.
243 * @param $code string
247 public static function isValidCode( $code ) {
249 strcspn( $code, ":/\\\000" ) === strlen( $code )
250 && !preg_match( Title
::getTitleInvalidRegex(), $code );
254 * Returns true if a language code is of a valid form for the purposes of
255 * internal customisation of MediaWiki, via Messages*.php.
257 * @param $code string
259 * @throws MWException
263 public static function isValidBuiltInCode( $code ) {
265 if( !is_string($code) ) {
266 $type = gettype( $code );
267 if( $type === 'object' ) {
268 $addmsg = " of class " . get_class( $code );
272 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
275 return preg_match( '/^[a-z0-9-]+$/i', $code );
280 * @return String Name of the language class
282 public static function classFromCode( $code ) {
283 if ( $code == 'en' ) {
286 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
291 * Includes language class files
293 * @param $class string Name of the language class
295 public static function preloadLanguageClass( $class ) {
298 if ( $class === 'Language' ) {
302 if ( !defined( 'MW_COMPILED' ) ) {
303 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
304 include_once( "$IP/languages/classes/$class.php" );
310 * Get the LocalisationCache instance
312 * @return LocalisationCache
314 public static function getLocalisationCache() {
315 if ( is_null( self
::$dataCache ) ) {
316 global $wgLocalisationCacheConf;
317 $class = $wgLocalisationCacheConf['class'];
318 self
::$dataCache = new $class( $wgLocalisationCacheConf );
320 return self
::$dataCache;
323 function __construct() {
324 $this->mConverter
= new FakeConverter( $this );
325 // Set the code to the name of the descendant
326 if ( get_class( $this ) == 'Language' ) {
329 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
331 self
::getLocalisationCache();
335 * Reduce memory usage
337 function __destruct() {
338 foreach ( $this as $name => $value ) {
339 unset( $this->$name );
344 * Hook which will be called if this is the content language.
345 * Descendants can use this to register hook functions or modify globals
347 function initContLang() { }
350 * Same as getFallbacksFor for current language.
352 * @deprecated in 1.19
354 function getFallbackLanguageCode() {
355 wfDeprecated( __METHOD__
);
356 return self
::getFallbackFor( $this->mCode
);
363 function getFallbackLanguages() {
364 return self
::getFallbacksFor( $this->mCode
);
368 * Exports $wgBookstoreListEn
371 function getBookstoreList() {
372 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
378 public function getNamespaces() {
379 if ( is_null( $this->namespaceNames
) ) {
380 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
382 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
383 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
385 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
387 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
388 if ( $wgMetaNamespaceTalk ) {
389 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
391 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
392 $this->namespaceNames
[NS_PROJECT_TALK
] =
393 $this->fixVariableInNamespace( $talk );
396 # Sometimes a language will be localised but not actually exist on this wiki.
397 foreach ( $this->namespaceNames
as $key => $text ) {
398 if ( !isset( $validNamespaces[$key] ) ) {
399 unset( $this->namespaceNames
[$key] );
403 # The above mixing may leave namespaces out of canonical order.
404 # Re-order by namespace ID number...
405 ksort( $this->namespaceNames
);
407 wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
409 return $this->namespaceNames
;
413 * Arbitrarily set all of the namespace names at once. Mainly used for testing
414 * @param $namespaces Array of namespaces (id => name)
416 public function setNamespaces( array $namespaces ) {
417 $this->namespaceNames
= $namespaces;
421 * A convenience function that returns the same thing as
422 * getNamespaces() except with the array values changed to ' '
423 * where it found '_', useful for producing output to be displayed
424 * e.g. in <select> forms.
428 function getFormattedNamespaces() {
429 $ns = $this->getNamespaces();
430 foreach ( $ns as $k => $v ) {
431 $ns[$k] = strtr( $v, '_', ' ' );
437 * Get a namespace value by key
439 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
440 * echo $mw_ns; // prints 'MediaWiki'
443 * @param $index Int: the array key of the namespace to return
444 * @return mixed, string if the namespace value exists, otherwise false
446 function getNsText( $index ) {
447 $ns = $this->getNamespaces();
448 return isset( $ns[$index] ) ?
$ns[$index] : false;
452 * A convenience function that returns the same thing as
453 * getNsText() except with '_' changed to ' ', useful for
456 * @param $index string
460 function getFormattedNsText( $index ) {
461 $ns = $this->getNsText( $index );
462 return strtr( $ns, '_', ' ' );
466 * Returns gender-dependent namespace alias if available.
467 * @param $index Int: namespace index
468 * @param $gender String: gender key (male, female... )
472 function getGenderNsText( $index, $gender ) {
473 global $wgExtraGenderNamespaces;
475 $ns = $wgExtraGenderNamespaces + self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
476 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
480 * Whether this language makes distinguishes genders for example in
485 function needsGenderDistinction() {
486 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
487 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
488 // $wgExtraGenderNamespaces overrides everything
490 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
491 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
492 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
495 // Check what is in i18n files
496 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
497 return count( $aliases ) > 0;
502 * Get a namespace key by value, case insensitive.
503 * Only matches namespace names for the current language, not the
504 * canonical ones defined in Namespace.php.
506 * @param $text String
507 * @return mixed An integer if $text is a valid value otherwise false
509 function getLocalNsIndex( $text ) {
510 $lctext = $this->lc( $text );
511 $ids = $this->getNamespaceIds();
512 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
518 function getNamespaceAliases() {
519 if ( is_null( $this->namespaceAliases
) ) {
520 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
524 foreach ( $aliases as $name => $index ) {
525 if ( $index === NS_PROJECT_TALK
) {
526 unset( $aliases[$name] );
527 $name = $this->fixVariableInNamespace( $name );
528 $aliases[$name] = $index;
533 global $wgExtraGenderNamespaces;
534 $genders = $wgExtraGenderNamespaces +
(array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
535 foreach ( $genders as $index => $forms ) {
536 foreach ( $forms as $alias ) {
537 $aliases[$alias] = $index;
541 $this->namespaceAliases
= $aliases;
543 return $this->namespaceAliases
;
549 function getNamespaceIds() {
550 if ( is_null( $this->mNamespaceIds
) ) {
551 global $wgNamespaceAliases;
552 # Put namespace names and aliases into a hashtable.
553 # If this is too slow, then we should arrange it so that it is done
554 # before caching. The catch is that at pre-cache time, the above
555 # class-specific fixup hasn't been done.
556 $this->mNamespaceIds
= array();
557 foreach ( $this->getNamespaces() as $index => $name ) {
558 $this->mNamespaceIds
[$this->lc( $name )] = $index;
560 foreach ( $this->getNamespaceAliases() as $name => $index ) {
561 $this->mNamespaceIds
[$this->lc( $name )] = $index;
563 if ( $wgNamespaceAliases ) {
564 foreach ( $wgNamespaceAliases as $name => $index ) {
565 $this->mNamespaceIds
[$this->lc( $name )] = $index;
569 return $this->mNamespaceIds
;
573 * Get a namespace key by value, case insensitive. Canonical namespace
574 * names override custom ones defined for the current language.
576 * @param $text String
577 * @return mixed An integer if $text is a valid value otherwise false
579 function getNsIndex( $text ) {
580 $lctext = $this->lc( $text );
581 $ns = MWNamespace
::getCanonicalIndex( $lctext );
582 if ( $ns !== null ) {
585 $ids = $this->getNamespaceIds();
586 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
590 * short names for language variants used for language conversion links.
592 * @param $code String
593 * @param $usemsg bool Use the "variantname-xyz" message if it exists
596 function getVariantname( $code, $usemsg = true ) {
597 $msg = "variantname-$code";
598 if ( $usemsg && wfMessage( $msg )->exists() ) {
599 return $this->getMessageFromDB( $msg );
601 $name = self
::fetchLanguageName( $code );
603 return $name; # if it's defined as a language name, show that
605 # otherwise, output the language code
611 * @param $name string
614 function specialPage( $name ) {
615 $aliases = $this->getSpecialPageAliases();
616 if ( isset( $aliases[$name][0] ) ) {
617 $name = $aliases[$name][0];
619 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
625 function getQuickbarSettings() {
627 $this->getMessage( 'qbsettings-none' ),
628 $this->getMessage( 'qbsettings-fixedleft' ),
629 $this->getMessage( 'qbsettings-fixedright' ),
630 $this->getMessage( 'qbsettings-floatingleft' ),
631 $this->getMessage( 'qbsettings-floatingright' ),
632 $this->getMessage( 'qbsettings-directionality' )
639 function getDatePreferences() {
640 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
646 function getDateFormats() {
647 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
651 * @return array|string
653 function getDefaultDateFormat() {
654 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
655 if ( $df === 'dmy or mdy' ) {
656 global $wgAmericanDates;
657 return $wgAmericanDates ?
'mdy' : 'dmy';
666 function getDatePreferenceMigrationMap() {
667 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
674 function getImageFile( $image ) {
675 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
681 function getExtraUserToggles() {
682 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
689 function getUserToggle( $tog ) {
690 return $this->getMessageFromDB( "tog-$tog" );
694 * Get native language names, indexed by code.
695 * Only those defined in MediaWiki, no other data like CLDR.
696 * If $customisedOnly is true, only returns codes with a messages file
698 * @param $customisedOnly bool
701 * @deprecated in 1.20, use fetchLanguageNames()
703 public static function getLanguageNames( $customisedOnly = false ) {
704 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
708 * Get translated language names. This is done on best effort and
709 * by default this is exactly the same as Language::getLanguageNames.
710 * The CLDR extension provides translated names.
711 * @param $code String Language code.
712 * @return Array language code => language name
714 * @deprecated in 1.20, use fetchLanguageNames()
716 public static function getTranslatedLanguageNames( $code ) {
717 return self
::fetchLanguageNames( $code, 'all' );
721 * Get an array of language names, indexed by code.
722 * @param $inLanguage null|string: Code of language in which to return the names
723 * Use null for autonyms (native names)
724 * @param $include string:
725 * 'all' all available languages
726 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
727 * 'mwfile' only if the language is in 'mw' *and* has a message file
728 * @return array: language code => language name
731 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
732 global $wgExtraLanguageNames;
733 static $coreLanguageNames;
735 if ( $coreLanguageNames === null ) {
736 include( MWInit
::compiledPath( 'languages/Names.php' ) );
742 # TODO: also include when $inLanguage is null, when this code is more efficient
743 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
746 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
747 foreach ( $mwNames as $mwCode => $mwName ) {
748 # - Prefer own MediaWiki native name when not using the hook
749 # - For other names just add if not added through the hook
750 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
751 $names[$mwCode] = $mwName;
755 if ( $include === 'all' ) {
760 $coreCodes = array_keys( $mwNames );
761 foreach( $coreCodes as $coreCode ) {
762 $returnMw[$coreCode] = $names[$coreCode];
765 if( $include === 'mwfile' ) {
766 $namesMwFile = array();
767 # We do this using a foreach over the codes instead of a directory
768 # loop so that messages files in extensions will work correctly.
769 foreach ( $returnMw as $code => $value ) {
770 if ( is_readable( self
::getMessagesFileName( $code ) ) ) {
771 $namesMwFile[$code] = $names[$code];
776 # 'mw' option; default if it's not one of the other two options (all/mwfile)
781 * @param $code string: The code of the language for which to get the name
782 * @param $inLanguage null|string: Code of language in which to return the name (null for autonyms)
783 * @param $include string: 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
784 * @return string: Language name or empty
787 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
788 $array = self
::fetchLanguageNames( $inLanguage, $include );
789 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
793 * Get a message from the MediaWiki namespace.
795 * @param $msg String: message name
798 function getMessageFromDB( $msg ) {
799 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
803 * Get the native language name of $code.
804 * Only if defined in MediaWiki, no other data like CLDR.
805 * @param $code string
807 * @deprecated in 1.20, use fetchLanguageName()
809 function getLanguageName( $code ) {
810 return self
::fetchLanguageName( $code );
817 function getMonthName( $key ) {
818 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
824 function getMonthNamesArray() {
825 $monthNames = array( '' );
826 for ( $i = 1; $i < 13; $i++
) {
827 $monthNames[] = $this->getMonthName( $i );
836 function getMonthNameGen( $key ) {
837 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
844 function getMonthAbbreviation( $key ) {
845 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
851 function getMonthAbbreviationsArray() {
852 $monthNames = array( '' );
853 for ( $i = 1; $i < 13; $i++
) {
854 $monthNames[] = $this->getMonthAbbreviation( $i );
863 function getWeekdayName( $key ) {
864 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
871 function getWeekdayAbbreviation( $key ) {
872 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
879 function getIranianCalendarMonthName( $key ) {
880 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
887 function getHebrewCalendarMonthName( $key ) {
888 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
895 function getHebrewCalendarMonthNameGen( $key ) {
896 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
903 function getHijriCalendarMonthName( $key ) {
904 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
908 * This is a workalike of PHP's date() function, but with better
909 * internationalisation, a reduced set of format characters, and a better
912 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
913 * PHP manual for definitions. There are a number of extensions, which
916 * xn Do not translate digits of the next numeric format character
917 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
918 * xr Use roman numerals for the next numeric format character
919 * xh Use hebrew numerals for the next numeric format character
921 * xg Genitive month name
923 * xij j (day number) in Iranian calendar
924 * xiF F (month name) in Iranian calendar
925 * xin n (month number) in Iranian calendar
926 * xiy y (two digit year) in Iranian calendar
927 * xiY Y (full year) in Iranian calendar
929 * xjj j (day number) in Hebrew calendar
930 * xjF F (month name) in Hebrew calendar
931 * xjt t (days in month) in Hebrew calendar
932 * xjx xg (genitive month name) in Hebrew calendar
933 * xjn n (month number) in Hebrew calendar
934 * xjY Y (full year) in Hebrew calendar
936 * xmj j (day number) in Hijri calendar
937 * xmF F (month name) in Hijri calendar
938 * xmn n (month number) in Hijri calendar
939 * xmY Y (full year) in Hijri calendar
941 * xkY Y (full year) in Thai solar calendar. Months and days are
942 * identical to the Gregorian calendar
943 * xoY Y (full year) in Minguo calendar or Juche year.
944 * Months and days are identical to the
946 * xtY Y (full year) in Japanese nengo. Months and days are
947 * identical to the Gregorian calendar
949 * Characters enclosed in double quotes will be considered literal (with
950 * the quotes themselves removed). Unmatched quotes will be considered
951 * literal quotes. Example:
953 * "The month is" F => The month is January
956 * Backslash escaping is also supported.
958 * Input timestamp is assumed to be pre-normalized to the desired local
961 * @param $format String
962 * @param $ts String: 14-character timestamp
965 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
969 function sprintfDate( $format, $ts ) {
982 for ( $p = 0; $p < strlen( $format ); $p++
) {
985 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
986 $code .= $format[++
$p];
989 if ( ( $code === 'xi' ||
$code == 'xj' ||
$code == 'xk' ||
$code == 'xm' ||
$code == 'xo' ||
$code == 'xt' ) && $p < strlen( $format ) - 1 ) {
990 $code .= $format[++
$p];
1001 $rawToggle = !$rawToggle;
1010 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1013 if ( !$hebrew ) $hebrew = self
::tsToHebrew( $ts );
1014 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1017 $num = substr( $ts, 6, 2 );
1020 if ( !$unix ) $unix = wfTimestamp( TS_UNIX
, $ts );
1021 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) +
1 );
1024 $num = intval( substr( $ts, 6, 2 ) );
1028 $iranian = self
::tsToIranian( $ts );
1034 $hijri = self
::tsToHijri( $ts );
1040 $hebrew = self
::tsToHebrew( $ts );
1046 $unix = wfTimestamp( TS_UNIX
, $ts );
1048 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) +
1 );
1052 $unix = wfTimestamp( TS_UNIX
, $ts );
1054 $w = gmdate( 'w', $unix );
1059 $unix = wfTimestamp( TS_UNIX
, $ts );
1061 $num = gmdate( 'w', $unix );
1065 $unix = wfTimestamp( TS_UNIX
, $ts );
1067 $num = gmdate( 'z', $unix );
1071 $unix = wfTimestamp( TS_UNIX
, $ts );
1073 $num = gmdate( 'W', $unix );
1076 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1080 $iranian = self
::tsToIranian( $ts );
1082 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1086 $hijri = self
::tsToHijri( $ts );
1088 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1092 $hebrew = self
::tsToHebrew( $ts );
1094 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1097 $num = substr( $ts, 4, 2 );
1100 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1103 $num = intval( substr( $ts, 4, 2 ) );
1107 $iranian = self
::tsToIranian( $ts );
1113 $hijri = self
::tsToHijri ( $ts );
1119 $hebrew = self
::tsToHebrew( $ts );
1125 $unix = wfTimestamp( TS_UNIX
, $ts );
1127 $num = gmdate( 't', $unix );
1131 $hebrew = self
::tsToHebrew( $ts );
1137 $unix = wfTimestamp( TS_UNIX
, $ts );
1139 $num = gmdate( 'L', $unix );
1143 $unix = wfTimestamp( TS_UNIX
, $ts );
1145 $num = gmdate( 'o', $unix );
1148 $num = substr( $ts, 0, 4 );
1152 $iranian = self
::tsToIranian( $ts );
1158 $hijri = self
::tsToHijri( $ts );
1164 $hebrew = self
::tsToHebrew( $ts );
1170 $thai = self
::tsToYear( $ts, 'thai' );
1176 $minguo = self
::tsToYear( $ts, 'minguo' );
1182 $tenno = self
::tsToYear( $ts, 'tenno' );
1187 $num = substr( $ts, 2, 2 );
1191 $iranian = self
::tsToIranian( $ts );
1193 $num = substr( $iranian[0], -2 );
1196 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1199 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1202 $h = substr( $ts, 8, 2 );
1203 $num = $h %
12 ?
$h %
12 : 12;
1206 $num = intval( substr( $ts, 8, 2 ) );
1209 $h = substr( $ts, 8, 2 );
1210 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1213 $num = substr( $ts, 8, 2 );
1216 $num = substr( $ts, 10, 2 );
1219 $num = substr( $ts, 12, 2 );
1223 $unix = wfTimestamp( TS_UNIX
, $ts );
1225 $s .= gmdate( 'c', $unix );
1229 $unix = wfTimestamp( TS_UNIX
, $ts );
1231 $s .= gmdate( 'r', $unix );
1235 $unix = wfTimestamp( TS_UNIX
, $ts );
1240 # Backslash escaping
1241 if ( $p < strlen( $format ) - 1 ) {
1242 $s .= $format[++
$p];
1249 if ( $p < strlen( $format ) - 1 ) {
1250 $endQuote = strpos( $format, '"', $p +
1 );
1251 if ( $endQuote === false ) {
1252 # No terminating quote, assume literal "
1255 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1259 # Quote at end of string, assume literal "
1266 if ( $num !== false ) {
1267 if ( $rawToggle ||
$raw ) {
1270 } elseif ( $roman ) {
1271 $s .= self
::romanNumeral( $num );
1273 } elseif ( $hebrewNum ) {
1274 $s .= self
::hebrewNumeral( $num );
1277 $s .= $this->formatNum( $num, true );
1284 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1285 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1288 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1289 * Gregorian dates to Iranian dates. Originally written in C, it
1290 * is released under the terms of GNU Lesser General Public
1291 * License. Conversion to PHP was performed by Niklas Laxström.
1293 * Link: http://www.farsiweb.info/jalali/jalali.c
1299 private static function tsToIranian( $ts ) {
1300 $gy = substr( $ts, 0, 4 ) -1600;
1301 $gm = substr( $ts, 4, 2 ) -1;
1302 $gd = substr( $ts, 6, 2 ) -1;
1304 # Days passed from the beginning (including leap years)
1306 +
floor( ( $gy +
3 ) / 4 )
1307 - floor( ( $gy +
99 ) / 100 )
1308 +
floor( ( $gy +
399 ) / 400 );
1310 // Add days of the past months of this year
1311 for ( $i = 0; $i < $gm; $i++
) {
1312 $gDayNo +
= self
::$GREG_DAYS[$i];
1316 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1320 // Days passed in current month
1321 $gDayNo +
= (int)$gd;
1323 $jDayNo = $gDayNo - 79;
1325 $jNp = floor( $jDayNo / 12053 );
1328 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1331 if ( $jDayNo >= 366 ) {
1332 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1333 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1336 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1337 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1343 return array( $jy, $jm, $jd );
1347 * Converting Gregorian dates to Hijri dates.
1349 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1351 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1357 private static function tsToHijri( $ts ) {
1358 $year = substr( $ts, 0, 4 );
1359 $month = substr( $ts, 4, 2 );
1360 $day = substr( $ts, 6, 2 );
1368 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1369 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1372 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1373 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1374 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1377 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1378 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1381 $zl = $zjd -1948440 +
10632;
1382 $zn = (int)( ( $zl - 1 ) / 10631 );
1383 $zl = $zl - 10631 * $zn +
354;
1384 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1385 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1386 $zm = (int)( ( 24 * $zl ) / 709 );
1387 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1388 $zy = 30 * $zn +
$zj - 30;
1390 return array( $zy, $zm, $zd );
1394 * Converting Gregorian dates to Hebrew dates.
1396 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1397 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1398 * to translate the relevant functions into PHP and release them under
1401 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1402 * and Adar II is 14. In a non-leap year, Adar is 6.
1408 private static function tsToHebrew( $ts ) {
1410 $year = substr( $ts, 0, 4 );
1411 $month = substr( $ts, 4, 2 );
1412 $day = substr( $ts, 6, 2 );
1414 # Calculate Hebrew year
1415 $hebrewYear = $year +
3760;
1417 # Month number when September = 1, August = 12
1419 if ( $month > 12 ) {
1426 # Calculate day of year from 1 September
1428 for ( $i = 1; $i < $month; $i++
) {
1432 # Check if the year is leap
1433 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1436 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1443 # Calculate the start of the Hebrew year
1444 $start = self
::hebrewYearStart( $hebrewYear );
1446 # Calculate next year's start
1447 if ( $dayOfYear <= $start ) {
1448 # Day is before the start of the year - it is the previous year
1450 $nextStart = $start;
1454 # Add days since previous year's 1 September
1456 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1460 # Start of the new (previous) year
1461 $start = self
::hebrewYearStart( $hebrewYear );
1464 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1467 # Calculate Hebrew day of year
1468 $hebrewDayOfYear = $dayOfYear - $start;
1470 # Difference between year's days
1471 $diff = $nextStart - $start;
1472 # Add 12 (or 13 for leap years) days to ignore the difference between
1473 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1474 # difference is only about the year type
1475 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1481 # Check the year pattern, and is leap year
1482 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1483 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1484 # and non-leap years
1485 $yearPattern = $diff %
30;
1486 # Check if leap year
1487 $isLeap = $diff >= 30;
1489 # Calculate day in the month from number of day in the Hebrew year
1490 # Don't check Adar - if the day is not in Adar, we will stop before;
1491 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1492 $hebrewDay = $hebrewDayOfYear;
1495 while ( $hebrewMonth <= 12 ) {
1496 # Calculate days in this month
1497 if ( $isLeap && $hebrewMonth == 6 ) {
1498 # Adar in a leap year
1500 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1502 if ( $hebrewDay <= $days ) {
1506 # Subtract the days of Adar I
1507 $hebrewDay -= $days;
1510 if ( $hebrewDay <= $days ) {
1516 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1517 # Cheshvan in a complete year (otherwise as the rule below)
1519 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1520 # Kislev in an incomplete year (otherwise as the rule below)
1523 # Odd months have 30 days, even have 29
1524 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1526 if ( $hebrewDay <= $days ) {
1527 # In the current month
1530 # Subtract the days of the current month
1531 $hebrewDay -= $days;
1532 # Try in the next month
1537 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1541 * This calculates the Hebrew year start, as days since 1 September.
1542 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1543 * Used for Hebrew date.
1549 private static function hebrewYearStart( $year ) {
1550 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1551 $b = intval( ( $year - 1 ) %
4 );
1552 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1556 $Mar = intval( $m );
1562 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1563 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1565 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1567 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1571 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1576 * Algorithm to convert Gregorian dates to Thai solar dates,
1577 * Minguo dates or Minguo dates.
1579 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1580 * http://en.wikipedia.org/wiki/Minguo_calendar
1581 * http://en.wikipedia.org/wiki/Japanese_era_name
1583 * @param $ts String: 14-character timestamp
1584 * @param $cName String: calender name
1585 * @return Array: converted year, month, day
1587 private static function tsToYear( $ts, $cName ) {
1588 $gy = substr( $ts, 0, 4 );
1589 $gm = substr( $ts, 4, 2 );
1590 $gd = substr( $ts, 6, 2 );
1592 if ( !strcmp( $cName, 'thai' ) ) {
1594 # Add 543 years to the Gregorian calendar
1595 # Months and days are identical
1596 $gy_offset = $gy +
543;
1597 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1599 # Deduct 1911 years from the Gregorian calendar
1600 # Months and days are identical
1601 $gy_offset = $gy - 1911;
1602 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1603 # Nengō dates up to Meiji period
1604 # Deduct years from the Gregorian calendar
1605 # depending on the nengo periods
1606 # Months and days are identical
1607 if ( ( $gy < 1912 ) ||
( ( $gy == 1912 ) && ( $gm < 7 ) ) ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1609 $gy_gannen = $gy - 1868 +
1;
1610 $gy_offset = $gy_gannen;
1611 if ( $gy_gannen == 1 ) {
1614 $gy_offset = '明治' . $gy_offset;
1616 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1617 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1618 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1619 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1620 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1624 $gy_gannen = $gy - 1912 +
1;
1625 $gy_offset = $gy_gannen;
1626 if ( $gy_gannen == 1 ) {
1629 $gy_offset = '大正' . $gy_offset;
1631 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1632 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1633 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1637 $gy_gannen = $gy - 1926 +
1;
1638 $gy_offset = $gy_gannen;
1639 if ( $gy_gannen == 1 ) {
1642 $gy_offset = '昭和' . $gy_offset;
1645 $gy_gannen = $gy - 1989 +
1;
1646 $gy_offset = $gy_gannen;
1647 if ( $gy_gannen == 1 ) {
1650 $gy_offset = '平成' . $gy_offset;
1656 return array( $gy_offset, $gm, $gd );
1660 * Roman number formatting up to 3000
1666 static function romanNumeral( $num ) {
1667 static $table = array(
1668 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1669 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1670 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1671 array( '', 'M', 'MM', 'MMM' )
1674 $num = intval( $num );
1675 if ( $num > 3000 ||
$num <= 0 ) {
1680 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1681 if ( $num >= $pow10 ) {
1682 $s .= $table[$i][(int)floor( $num / $pow10 )];
1684 $num = $num %
$pow10;
1690 * Hebrew Gematria number formatting up to 9999
1696 static function hebrewNumeral( $num ) {
1697 static $table = array(
1698 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1699 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1700 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1701 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1704 $num = intval( $num );
1705 if ( $num > 9999 ||
$num <= 0 ) {
1710 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1711 if ( $num >= $pow10 ) {
1712 if ( $num == 15 ||
$num == 16 ) {
1713 $s .= $table[0][9] . $table[0][$num - 9];
1716 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1717 if ( $pow10 == 1000 ) {
1722 $num = $num %
$pow10;
1724 if ( strlen( $s ) == 2 ) {
1727 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1728 $str .= substr( $s, strlen( $s ) - 2, 2 );
1730 $start = substr( $str, 0, strlen( $str ) - 2 );
1731 $end = substr( $str, strlen( $str ) - 2 );
1734 $str = $start . 'ך';
1737 $str = $start . 'ם';
1740 $str = $start . 'ן';
1743 $str = $start . 'ף';
1746 $str = $start . 'ץ';
1753 * Used by date() and time() to adjust the time output.
1755 * @param $ts Int the time in date('YmdHis') format
1756 * @param $tz Mixed: adjust the time by this amount (default false, mean we
1757 * get user timecorrection setting)
1760 function userAdjust( $ts, $tz = false ) {
1761 global $wgUser, $wgLocaltimezone, $wgLocalTZoffset;
1763 if ( $tz === false ) {
1764 $tz = $wgUser->getOption( 'timecorrection' );
1767 $data = explode( '|', $tz, 3 );
1769 if ( $data[0] == 'System' ||
$tz == '' ) {
1771 if ( isset( $wgLocaltimezone ) ) {
1772 $data[0] = 'ZoneInfo';
1773 $data[2] = $wgLocaltimezone;
1775 # Global offset in minutes.
1776 if ( isset( $wgLocalTZoffset ) ) {
1777 $data[1] = $wgLocalTZoffset;
1781 if ( $data[0] == 'ZoneInfo' ) {
1782 wfSuppressWarnings();
1783 $userTZ = timezone_open( $data[2] );
1784 wfRestoreWarnings();
1785 if ( $userTZ !== false ) {
1786 $date = date_create( $ts, timezone_open( 'UTC' ) );
1787 date_timezone_set( $date, $userTZ );
1788 $date = date_format( $date, 'YmdHis' );
1791 # Unrecognized timezone, default to 'Offset' with the stored offset.
1792 $data[0] = 'Offset';
1796 if ( $data[0] == 'Offset' ) {
1797 $minDiff = intval( $data[1] );
1799 $data = explode( ':', $tz );
1800 if ( count( $data ) == 2 ) {
1801 $data[0] = intval( $data[0] );
1802 $data[1] = intval( $data[1] );
1803 $minDiff = abs( $data[0] ) * 60 +
$data[1];
1804 if ( $data[0] < 0 ) {
1805 $minDiff = -$minDiff;
1808 $minDiff = intval( $data[0] ) * 60;
1812 # No difference ? Return time unchanged
1813 if ( 0 == $minDiff ) {
1817 wfSuppressWarnings(); // E_STRICT system time bitching
1818 # Generate an adjusted date; take advantage of the fact that mktime
1819 # will normalize out-of-range values so we don't have to split $minDiff
1820 # into hours and minutes.
1822 (int)substr( $ts, 8, 2 ) ), # Hours
1823 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
1824 (int)substr( $ts, 12, 2 ), # Seconds
1825 (int)substr( $ts, 4, 2 ), # Month
1826 (int)substr( $ts, 6, 2 ), # Day
1827 (int)substr( $ts, 0, 4 ) ); # Year
1829 $date = date( 'YmdHis', $t );
1830 wfRestoreWarnings();
1836 * This is meant to be used by time(), date(), and timeanddate() to get
1837 * the date preference they're supposed to use, it should be used in
1841 * function timeanddate([...], $format = true) {
1842 * $datePreference = $this->dateFormat($format);
1847 * @param $usePrefs Mixed: if true, the user's preference is used
1848 * if false, the site/language default is used
1849 * if int/string, assumed to be a format.
1852 function dateFormat( $usePrefs = true ) {
1855 if ( is_bool( $usePrefs ) ) {
1857 $datePreference = $wgUser->getDatePreference();
1859 $datePreference = (string)User
::getDefaultOption( 'date' );
1862 $datePreference = (string)$usePrefs;
1866 if ( $datePreference == '' ) {
1870 return $datePreference;
1874 * Get a format string for a given type and preference
1875 * @param $type string May be date, time or both
1876 * @param $pref string The format name as it appears in Messages*.php
1880 function getDateFormatString( $type, $pref ) {
1881 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
1882 if ( $pref == 'default' ) {
1883 $pref = $this->getDefaultDateFormat();
1884 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1886 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1887 if ( is_null( $df ) ) {
1888 $pref = $this->getDefaultDateFormat();
1889 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
1892 $this->dateFormatStrings
[$type][$pref] = $df;
1894 return $this->dateFormatStrings
[$type][$pref];
1898 * @param $ts Mixed: the time format which needs to be turned into a
1899 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1900 * @param $adj Bool: whether to adjust the time output according to the
1901 * user configured offset ($timecorrection)
1902 * @param $format Mixed: true to use user's date format preference
1903 * @param $timecorrection String|bool the time offset as returned by
1904 * validateTimeZone() in Special:Preferences
1907 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1908 $ts = wfTimestamp( TS_MW
, $ts );
1910 $ts = $this->userAdjust( $ts, $timecorrection );
1912 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1913 return $this->sprintfDate( $df, $ts );
1917 * @param $ts Mixed: the time format which needs to be turned into a
1918 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1919 * @param $adj Bool: whether to adjust the time output according to the
1920 * user configured offset ($timecorrection)
1921 * @param $format Mixed: true to use user's date format preference
1922 * @param $timecorrection String|bool the time offset as returned by
1923 * validateTimeZone() in Special:Preferences
1926 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1927 $ts = wfTimestamp( TS_MW
, $ts );
1929 $ts = $this->userAdjust( $ts, $timecorrection );
1931 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1932 return $this->sprintfDate( $df, $ts );
1936 * @param $ts Mixed: the time format which needs to be turned into a
1937 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1938 * @param $adj Bool: whether to adjust the time output according to the
1939 * user configured offset ($timecorrection)
1940 * @param $format Mixed: what format to return, if it's false output the
1941 * default one (default true)
1942 * @param $timecorrection String|bool the time offset as returned by
1943 * validateTimeZone() in Special:Preferences
1946 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1947 $ts = wfTimestamp( TS_MW
, $ts );
1949 $ts = $this->userAdjust( $ts, $timecorrection );
1951 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1952 return $this->sprintfDate( $df, $ts );
1956 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
1960 * @param integer $seconds The amount of seconds.
1961 * @param array $chosenIntervals The intervals to enable.
1965 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
1966 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
1968 $segments = array();
1970 foreach ( $intervals as $intervalName => $intervalValue ) {
1971 $message = new Message( 'duration-' . $intervalName, array( $intervalValue ) );
1972 $segments[] = $message->inLanguage( $this )->escaped();
1975 return $this->listToText( $segments );
1979 * Takes a number of seconds and returns an array with a set of corresponding intervals.
1980 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
1984 * @param integer $seconds The amount of seconds.
1985 * @param array $chosenIntervals The intervals to enable.
1989 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
1990 if ( empty( $chosenIntervals ) ) {
1991 $chosenIntervals = array( 'millennia', 'centuries', 'decades', 'years', 'days', 'hours', 'minutes', 'seconds' );
1994 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
1995 $sortedNames = array_keys( $intervals );
1996 $smallestInterval = array_pop( $sortedNames );
1998 $segments = array();
2000 foreach ( $intervals as $name => $length ) {
2001 $value = floor( $seconds / $length );
2003 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2004 $seconds -= $value * $length;
2005 $segments[$name] = $value;
2013 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2015 * @param $type String: can be 'date', 'time' or 'both'
2016 * @param $ts Mixed: the time format which needs to be turned into a
2017 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2018 * @param $user User object used to get preferences for timezone and format
2019 * @param $options Array, can contain the following keys:
2020 * - 'timecorrection': time correction, can have the following values:
2021 * - true: use user's preference
2022 * - false: don't use time correction
2023 * - integer: value of time correction in minutes
2024 * - 'format': format to use, can have the following values:
2025 * - true: use user's preference
2026 * - false: use default preference
2027 * - string: format to use
2031 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2032 $ts = wfTimestamp( TS_MW
, $ts );
2033 $options +
= array( 'timecorrection' => true, 'format' => true );
2034 if ( $options['timecorrection'] !== false ) {
2035 if ( $options['timecorrection'] === true ) {
2036 $offset = $user->getOption( 'timecorrection' );
2038 $offset = $options['timecorrection'];
2040 $ts = $this->userAdjust( $ts, $offset );
2042 if ( $options['format'] === true ) {
2043 $format = $user->getDatePreference();
2045 $format = $options['format'];
2047 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2048 return $this->sprintfDate( $df, $ts );
2052 * Get the formatted date for the given timestamp and formatted for
2055 * @param $ts Mixed: the time format which needs to be turned into a
2056 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2057 * @param $user User object used to get preferences for timezone and format
2058 * @param $options Array, can contain the following keys:
2059 * - 'timecorrection': time correction, can have the following values:
2060 * - true: use user's preference
2061 * - false: don't use time correction
2062 * - integer: value of time correction in minutes
2063 * - 'format': format to use, can have the following values:
2064 * - true: use user's preference
2065 * - false: use default preference
2066 * - string: format to use
2070 public function userDate( $ts, User
$user, array $options = array() ) {
2071 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2075 * Get the formatted time for the given timestamp and formatted for
2078 * @param $ts Mixed: the time format which needs to be turned into a
2079 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2080 * @param $user User object used to get preferences for timezone and format
2081 * @param $options Array, can contain the following keys:
2082 * - 'timecorrection': time correction, can have the following values:
2083 * - true: use user's preference
2084 * - false: don't use time correction
2085 * - integer: value of time correction in minutes
2086 * - 'format': format to use, can have the following values:
2087 * - true: use user's preference
2088 * - false: use default preference
2089 * - string: format to use
2093 public function userTime( $ts, User
$user, array $options = array() ) {
2094 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2098 * Get the formatted date and time for the given timestamp and formatted for
2101 * @param $ts Mixed: the time format which needs to be turned into a
2102 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2103 * @param $user User object used to get preferences for timezone and format
2104 * @param $options Array, can contain the following keys:
2105 * - 'timecorrection': time correction, can have the following values:
2106 * - true: use user's preference
2107 * - false: don't use time correction
2108 * - integer: value of time correction in minutes
2109 * - 'format': format to use, can have the following values:
2110 * - true: use user's preference
2111 * - false: use default preference
2112 * - string: format to use
2116 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2117 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2121 * @param $key string
2122 * @return array|null
2124 function getMessage( $key ) {
2125 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2131 function getAllMessages() {
2132 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2141 function iconv( $in, $out, $string ) {
2142 # This is a wrapper for iconv in all languages except esperanto,
2143 # which does some nasty x-conversions beforehand
2145 # Even with //IGNORE iconv can whine about illegal characters in
2146 # *input* string. We just ignore those too.
2147 # REF: http://bugs.php.net/bug.php?id=37166
2148 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2149 wfSuppressWarnings();
2150 $text = iconv( $in, $out . '//IGNORE', $string );
2151 wfRestoreWarnings();
2155 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2158 * @param $matches array
2159 * @return mixed|string
2161 function ucwordbreaksCallbackAscii( $matches ) {
2162 return $this->ucfirst( $matches[1] );
2166 * @param $matches array
2169 function ucwordbreaksCallbackMB( $matches ) {
2170 return mb_strtoupper( $matches[0] );
2174 * @param $matches array
2177 function ucCallback( $matches ) {
2178 list( $wikiUpperChars ) = self
::getCaseMaps();
2179 return strtr( $matches[1], $wikiUpperChars );
2183 * @param $matches array
2186 function lcCallback( $matches ) {
2187 list( , $wikiLowerChars ) = self
::getCaseMaps();
2188 return strtr( $matches[1], $wikiLowerChars );
2192 * @param $matches array
2195 function ucwordsCallbackMB( $matches ) {
2196 return mb_strtoupper( $matches[0] );
2200 * @param $matches array
2203 function ucwordsCallbackWiki( $matches ) {
2204 list( $wikiUpperChars ) = self
::getCaseMaps();
2205 return strtr( $matches[0], $wikiUpperChars );
2209 * Make a string's first character uppercase
2211 * @param $str string
2215 function ucfirst( $str ) {
2217 if ( $o < 96 ) { // if already uppercase...
2219 } elseif ( $o < 128 ) {
2220 return ucfirst( $str ); // use PHP's ucfirst()
2222 // fall back to more complex logic in case of multibyte strings
2223 return $this->uc( $str, true );
2228 * Convert a string to uppercase
2230 * @param $str string
2231 * @param $first bool
2235 function uc( $str, $first = false ) {
2236 if ( function_exists( 'mb_strtoupper' ) ) {
2238 if ( $this->isMultibyte( $str ) ) {
2239 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2241 return ucfirst( $str );
2244 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2247 if ( $this->isMultibyte( $str ) ) {
2248 $x = $first ?
'^' : '';
2249 return preg_replace_callback(
2250 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2251 array( $this, 'ucCallback' ),
2255 return $first ?
ucfirst( $str ) : strtoupper( $str );
2261 * @param $str string
2262 * @return mixed|string
2264 function lcfirst( $str ) {
2267 return strval( $str );
2268 } elseif ( $o >= 128 ) {
2269 return $this->lc( $str, true );
2270 } elseif ( $o > 96 ) {
2273 $str[0] = strtolower( $str[0] );
2279 * @param $str string
2280 * @param $first bool
2281 * @return mixed|string
2283 function lc( $str, $first = false ) {
2284 if ( function_exists( 'mb_strtolower' ) ) {
2286 if ( $this->isMultibyte( $str ) ) {
2287 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2289 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2292 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2295 if ( $this->isMultibyte( $str ) ) {
2296 $x = $first ?
'^' : '';
2297 return preg_replace_callback(
2298 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2299 array( $this, 'lcCallback' ),
2303 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2309 * @param $str string
2312 function isMultibyte( $str ) {
2313 return (bool)preg_match( '/[\x80-\xff]/', $str );
2317 * @param $str string
2318 * @return mixed|string
2320 function ucwords( $str ) {
2321 if ( $this->isMultibyte( $str ) ) {
2322 $str = $this->lc( $str );
2324 // regexp to find first letter in each word (i.e. after each space)
2325 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2327 // function to use to capitalize a single char
2328 if ( function_exists( 'mb_strtoupper' ) ) {
2329 return preg_replace_callback(
2331 array( $this, 'ucwordsCallbackMB' ),
2335 return preg_replace_callback(
2337 array( $this, 'ucwordsCallbackWiki' ),
2342 return ucwords( strtolower( $str ) );
2347 * capitalize words at word breaks
2349 * @param $str string
2352 function ucwordbreaks( $str ) {
2353 if ( $this->isMultibyte( $str ) ) {
2354 $str = $this->lc( $str );
2356 // since \b doesn't work for UTF-8, we explicitely define word break chars
2357 $breaks = "[ \-\(\)\}\{\.,\?!]";
2359 // find first letter after word break
2360 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2362 if ( function_exists( 'mb_strtoupper' ) ) {
2363 return preg_replace_callback(
2365 array( $this, 'ucwordbreaksCallbackMB' ),
2369 return preg_replace_callback(
2371 array( $this, 'ucwordsCallbackWiki' ),
2376 return preg_replace_callback(
2377 '/\b([\w\x80-\xff]+)\b/',
2378 array( $this, 'ucwordbreaksCallbackAscii' ),
2385 * Return a case-folded representation of $s
2387 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2388 * and $s2 are the same except for the case of their characters. It is not
2389 * necessary for the value returned to make sense when displayed.
2391 * Do *not* perform any other normalisation in this function. If a caller
2392 * uses this function when it should be using a more general normalisation
2393 * function, then fix the caller.
2399 function caseFold( $s ) {
2400 return $this->uc( $s );
2407 function checkTitleEncoding( $s ) {
2408 if ( is_array( $s ) ) {
2409 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2411 # Check for non-UTF-8 URLs
2412 $ishigh = preg_match( '/[\x80-\xff]/', $s );
2417 if ( function_exists( 'mb_check_encoding' ) ) {
2418 $isutf8 = mb_check_encoding( $s, 'UTF-8' );
2420 $isutf8 = preg_match( '/^(?>[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2421 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
2427 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2433 function fallback8bitEncoding() {
2434 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2438 * Most writing systems use whitespace to break up words.
2439 * Some languages such as Chinese don't conventionally do this,
2440 * which requires special handling when breaking up words for
2445 function hasWordBreaks() {
2450 * Some languages such as Chinese require word segmentation,
2451 * Specify such segmentation when overridden in derived class.
2453 * @param $string String
2456 function segmentByWord( $string ) {
2461 * Some languages have special punctuation need to be normalized.
2462 * Make such changes here.
2464 * @param $string String
2467 function normalizeForSearch( $string ) {
2468 return self
::convertDoubleWidth( $string );
2472 * convert double-width roman characters to single-width.
2473 * range: ff00-ff5f ~= 0020-007f
2475 * @param $string string
2479 protected static function convertDoubleWidth( $string ) {
2480 static $full = null;
2481 static $half = null;
2483 if ( $full === null ) {
2484 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2485 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2486 $full = str_split( $fullWidth, 3 );
2487 $half = str_split( $halfWidth );
2490 $string = str_replace( $full, $half, $string );
2495 * @param $string string
2496 * @param $pattern string
2499 protected static function insertSpace( $string, $pattern ) {
2500 $string = preg_replace( $pattern, " $1 ", $string );
2501 $string = preg_replace( '/ +/', ' ', $string );
2506 * @param $termsArray array
2509 function convertForSearchResult( $termsArray ) {
2510 # some languages, e.g. Chinese, need to do a conversion
2511 # in order for search results to be displayed correctly
2516 * Get the first character of a string.
2521 function firstChar( $s ) {
2524 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2525 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2530 if ( isset( $matches[1] ) ) {
2531 if ( strlen( $matches[1] ) != 3 ) {
2535 // Break down Hangul syllables to grab the first jamo
2536 $code = utf8ToCodepoint( $matches[1] );
2537 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
2539 } elseif ( $code < 0xb098 ) {
2540 return "\xe3\x84\xb1";
2541 } elseif ( $code < 0xb2e4 ) {
2542 return "\xe3\x84\xb4";
2543 } elseif ( $code < 0xb77c ) {
2544 return "\xe3\x84\xb7";
2545 } elseif ( $code < 0xb9c8 ) {
2546 return "\xe3\x84\xb9";
2547 } elseif ( $code < 0xbc14 ) {
2548 return "\xe3\x85\x81";
2549 } elseif ( $code < 0xc0ac ) {
2550 return "\xe3\x85\x82";
2551 } elseif ( $code < 0xc544 ) {
2552 return "\xe3\x85\x85";
2553 } elseif ( $code < 0xc790 ) {
2554 return "\xe3\x85\x87";
2555 } elseif ( $code < 0xcc28 ) {
2556 return "\xe3\x85\x88";
2557 } elseif ( $code < 0xce74 ) {
2558 return "\xe3\x85\x8a";
2559 } elseif ( $code < 0xd0c0 ) {
2560 return "\xe3\x85\x8b";
2561 } elseif ( $code < 0xd30c ) {
2562 return "\xe3\x85\x8c";
2563 } elseif ( $code < 0xd558 ) {
2564 return "\xe3\x85\x8d";
2566 return "\xe3\x85\x8e";
2573 function initEncoding() {
2574 # Some languages may have an alternate char encoding option
2575 # (Esperanto X-coding, Japanese furigana conversion, etc)
2576 # If this language is used as the primary content language,
2577 # an override to the defaults can be set here on startup.
2584 function recodeForEdit( $s ) {
2585 # For some languages we'll want to explicitly specify
2586 # which characters make it into the edit box raw
2587 # or are converted in some way or another.
2588 global $wgEditEncoding;
2589 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
2592 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2600 function recodeInput( $s ) {
2601 # Take the previous into account.
2602 global $wgEditEncoding;
2603 if ( $wgEditEncoding != '' ) {
2604 $enc = $wgEditEncoding;
2608 if ( $enc == 'UTF-8' ) {
2611 return $this->iconv( $enc, 'UTF-8', $s );
2616 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2617 * also cleans up certain backwards-compatible sequences, converting them
2618 * to the modern Unicode equivalent.
2620 * This is language-specific for performance reasons only.
2626 function normalize( $s ) {
2627 global $wgAllUnicodeFixes;
2628 $s = UtfNormal
::cleanUp( $s );
2629 if ( $wgAllUnicodeFixes ) {
2630 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2631 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2638 * Transform a string using serialized data stored in the given file (which
2639 * must be in the serialized subdirectory of $IP). The file contains pairs
2640 * mapping source characters to destination characters.
2642 * The data is cached in process memory. This will go faster if you have the
2643 * FastStringSearch extension.
2645 * @param $file string
2646 * @param $string string
2648 * @throws MWException
2651 function transformUsingPairFile( $file, $string ) {
2652 if ( !isset( $this->transformData
[$file] ) ) {
2653 $data = wfGetPrecompiledData( $file );
2654 if ( $data === false ) {
2655 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
2657 $this->transformData
[$file] = new ReplacementArray( $data );
2659 return $this->transformData
[$file]->replace( $string );
2663 * For right-to-left language support
2668 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
2672 * Return the correct HTML 'dir' attribute value for this language.
2676 return $this->isRTL() ?
'rtl' : 'ltr';
2680 * Return 'left' or 'right' as appropriate alignment for line-start
2681 * for this language's text direction.
2683 * Should be equivalent to CSS3 'start' text-align value....
2687 function alignStart() {
2688 return $this->isRTL() ?
'right' : 'left';
2692 * Return 'right' or 'left' as appropriate alignment for line-end
2693 * for this language's text direction.
2695 * Should be equivalent to CSS3 'end' text-align value....
2699 function alignEnd() {
2700 return $this->isRTL() ?
'left' : 'right';
2704 * A hidden direction mark (LRM or RLM), depending on the language direction.
2705 * Unlike getDirMark(), this function returns the character as an HTML entity.
2706 * This function should be used when the output is guaranteed to be HTML,
2707 * because it makes the output HTML source code more readable. When
2708 * the output is plain text or can be escaped, getDirMark() should be used.
2710 * @param $opposite Boolean Get the direction mark opposite to your language
2713 function getDirMarkEntity( $opposite = false ) {
2714 if ( $opposite ) { return $this->isRTL() ?
'‎' : '‏'; }
2715 return $this->isRTL() ?
'‏' : '‎';
2719 * A hidden direction mark (LRM or RLM), depending on the language direction.
2720 * This function produces them as invisible Unicode characters and
2721 * the output may be hard to read and debug, so it should only be used
2722 * when the output is plain text or can be escaped. When the output is
2723 * HTML, use getDirMarkEntity() instead.
2725 * @param $opposite Boolean Get the direction mark opposite to your language
2728 function getDirMark( $opposite = false ) {
2729 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
2730 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
2731 if ( $opposite ) { return $this->isRTL() ?
$lrm : $rlm; }
2732 return $this->isRTL() ?
$rlm : $lrm;
2738 function capitalizeAllNouns() {
2739 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
2743 * An arrow, depending on the language direction.
2745 * @param $direction String: the direction of the arrow: forwards (default), backwards, left, right, up, down.
2748 function getArrow( $direction = 'forwards' ) {
2749 switch ( $direction ) {
2751 return $this->isRTL() ?
'←' : '→';
2753 return $this->isRTL() ?
'→' : '←';
2766 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2770 function linkPrefixExtension() {
2771 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
2777 function getMagicWords() {
2778 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
2781 protected function doMagicHook() {
2782 if ( $this->mMagicHookDone
) {
2785 $this->mMagicHookDone
= true;
2786 wfProfileIn( 'LanguageGetMagic' );
2787 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
2788 wfProfileOut( 'LanguageGetMagic' );
2792 * Fill a MagicWord object with data from here
2796 function getMagic( $mw ) {
2797 $this->doMagicHook();
2799 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
2800 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
2802 $magicWords = $this->getMagicWords();
2803 if ( isset( $magicWords[$mw->mId
] ) ) {
2804 $rawEntry = $magicWords[$mw->mId
];
2810 if ( !is_array( $rawEntry ) ) {
2811 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2813 $mw->mCaseSensitive
= $rawEntry[0];
2814 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
2819 * Add magic words to the extension array
2821 * @param $newWords array
2823 function addMagicWordsByLang( $newWords ) {
2824 $fallbackChain = $this->getFallbackLanguages();
2825 $fallbackChain = array_reverse( $fallbackChain );
2826 foreach ( $fallbackChain as $code ) {
2827 if ( isset( $newWords[$code] ) ) {
2828 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
2834 * Get special page names, as an associative array
2835 * case folded alias => real name
2837 function getSpecialPageAliases() {
2838 // Cache aliases because it may be slow to load them
2839 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
2841 $this->mExtendedSpecialPageAliases
=
2842 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
2843 wfRunHooks( 'LanguageGetSpecialPageAliases',
2844 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
2847 return $this->mExtendedSpecialPageAliases
;
2851 * Italic is unsuitable for some languages
2853 * @param $text String: the text to be emphasized.
2856 function emphasize( $text ) {
2857 return "<em>$text</em>";
2861 * Normally we output all numbers in plain en_US style, that is
2862 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2863 * point twohundredthirtyfive. However this is not suitable for all
2864 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2865 * Icelandic just want to use commas instead of dots, and dots instead
2866 * of commas like "293.291,235".
2868 * An example of this function being called:
2870 * wfMsg( 'message', $wgLang->formatNum( $num ) )
2873 * See LanguageGu.php for the Gujarati implementation and
2874 * $separatorTransformTable on MessageIs.php for
2875 * the , => . and . => , implementation.
2877 * @todo check if it's viable to use localeconv() for the decimal
2879 * @param $number Mixed: the string to be formatted, should be an integer
2880 * or a floating point number.
2881 * @param $nocommafy Bool: set to true for special numbers like dates
2884 public function formatNum( $number, $nocommafy = false ) {
2885 global $wgTranslateNumerals;
2886 if ( !$nocommafy ) {
2887 $number = $this->commafy( $number );
2888 $s = $this->separatorTransformTable();
2890 $number = strtr( $number, $s );
2894 if ( $wgTranslateNumerals ) {
2895 $s = $this->digitTransformTable();
2897 $number = strtr( $number, $s );
2905 * @param $number string
2908 function parseFormattedNumber( $number ) {
2909 $s = $this->digitTransformTable();
2911 $number = strtr( $number, array_flip( $s ) );
2914 $s = $this->separatorTransformTable();
2916 $number = strtr( $number, array_flip( $s ) );
2919 $number = strtr( $number, array( ',' => '' ) );
2924 * Adds commas to a given number
2929 function commafy( $_ ) {
2930 $digitGroupingPattern = $this->digitGroupingPattern();
2931 if ( $_ === null ) {
2935 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
2936 // default grouping is at thousands, use the same for ###,###,### pattern too.
2937 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $_ ) ) );
2939 // Ref: http://cldr.unicode.org/translation/number-patterns
2941 if ( intval( $_ ) < 0 ) {
2942 // For negative numbers apply the algorithm like positive number and add sign.
2944 $_ = substr( $_, 1 );
2946 $numberpart = array();
2947 $decimalpart = array();
2948 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
2949 preg_match( "/\d+/", $_, $numberpart );
2950 preg_match( "/\.\d*/", $_, $decimalpart );
2951 $groupedNumber = ( count( $decimalpart ) > 0 ) ?
$decimalpart[0]:"";
2952 if ( $groupedNumber === $_ ) {
2953 // the string does not have any number part. Eg: .12345
2954 return $sign . $groupedNumber;
2956 $start = $end = strlen( $numberpart[0] );
2957 while ( $start > 0 ) {
2958 $match = $matches[0][$numMatches -1] ;
2959 $matchLen = strlen( $match );
2960 $start = $end - $matchLen;
2964 $groupedNumber = substr( $_ , $start, $end -$start ) . $groupedNumber ;
2966 if ( $numMatches > 1 ) {
2967 // use the last pattern for the rest of the number
2971 $groupedNumber = "," . $groupedNumber;
2974 return $sign . $groupedNumber;
2980 function digitGroupingPattern() {
2981 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
2987 function digitTransformTable() {
2988 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
2994 function separatorTransformTable() {
2995 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
2999 * Take a list of strings and build a locale-friendly comma-separated
3000 * list, using the local comma-separator message.
3001 * The last two strings are chained with an "and".
3006 function listToText( array $l ) {
3008 $m = count( $l ) - 1;
3010 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
3012 for ( $i = $m; $i >= 0; $i-- ) {
3015 } elseif ( $i == $m - 1 ) {
3016 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
3018 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
3026 * Take a list of strings and build a locale-friendly comma-separated
3027 * list, using the local comma-separator message.
3028 * @param $list array of strings to put in a comma list
3031 function commaList( array $list ) {
3035 array( 'parsemag', 'escapenoentities', 'language' => $this )
3042 * Take a list of strings and build a locale-friendly semicolon-separated
3043 * list, using the local semicolon-separator message.
3044 * @param $list array of strings to put in a semicolon list
3047 function semicolonList( array $list ) {
3050 'semicolon-separator',
3051 array( 'parsemag', 'escapenoentities', 'language' => $this )
3058 * Same as commaList, but separate it with the pipe instead.
3059 * @param $list array of strings to put in a pipe list
3062 function pipeList( array $list ) {
3066 array( 'escapenoentities', 'language' => $this )
3073 * Truncate a string to a specified length in bytes, appending an optional
3074 * string (e.g. for ellipses)
3076 * The database offers limited byte lengths for some columns in the database;
3077 * multi-byte character sets mean we need to ensure that only whole characters
3078 * are included, otherwise broken characters can be passed to the user
3080 * If $length is negative, the string will be truncated from the beginning
3082 * @param $string String to truncate
3083 * @param $length Int: maximum length (including ellipses)
3084 * @param $ellipsis String to append to the truncated text
3085 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
3086 * $adjustLength was introduced in 1.18, before that behaved as if false.
3089 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3090 # Use the localized ellipsis character
3091 if ( $ellipsis == '...' ) {
3092 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
3094 # Check if there is no need to truncate
3095 if ( $length == 0 ) {
3096 return $ellipsis; // convention
3097 } elseif ( strlen( $string ) <= abs( $length ) ) {
3098 return $string; // no need to truncate
3100 $stringOriginal = $string;
3101 # If ellipsis length is >= $length then we can't apply $adjustLength
3102 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3103 $string = $ellipsis; // this can be slightly unexpected
3104 # Otherwise, truncate and add ellipsis...
3106 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3107 if ( $length > 0 ) {
3108 $length -= $eLength;
3109 $string = substr( $string, 0, $length ); // xyz...
3110 $string = $this->removeBadCharLast( $string );
3111 $string = $string . $ellipsis;
3113 $length +
= $eLength;
3114 $string = substr( $string, $length ); // ...xyz
3115 $string = $this->removeBadCharFirst( $string );
3116 $string = $ellipsis . $string;
3119 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3120 # This check is *not* redundant if $adjustLength, due to the single case where
3121 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3122 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3125 return $stringOriginal;
3130 * Remove bytes that represent an incomplete Unicode character
3131 * at the end of string (e.g. bytes of the char are missing)
3133 * @param $string String
3136 protected function removeBadCharLast( $string ) {
3137 if ( $string != '' ) {
3138 $char = ord( $string[strlen( $string ) - 1] );
3140 if ( $char >= 0xc0 ) {
3141 # We got the first byte only of a multibyte char; remove it.
3142 $string = substr( $string, 0, -1 );
3143 } elseif ( $char >= 0x80 &&
3144 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3145 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
3147 # We chopped in the middle of a character; remove it
3155 * Remove bytes that represent an incomplete Unicode character
3156 * at the start of string (e.g. bytes of the char are missing)
3158 * @param $string String
3161 protected function removeBadCharFirst( $string ) {
3162 if ( $string != '' ) {
3163 $char = ord( $string[0] );
3164 if ( $char >= 0x80 && $char < 0xc0 ) {
3165 # We chopped in the middle of a character; remove the whole thing
3166 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3173 * Truncate a string of valid HTML to a specified length in bytes,
3174 * appending an optional string (e.g. for ellipses), and return valid HTML
3176 * This is only intended for styled/linked text, such as HTML with
3177 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3178 * Also, this will not detect things like "display:none" CSS.
3180 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3182 * @param string $text HTML string to truncate
3183 * @param int $length (zero/positive) Maximum length (including ellipses)
3184 * @param string $ellipsis String to append to the truncated text
3187 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3188 # Use the localized ellipsis character
3189 if ( $ellipsis == '...' ) {
3190 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
3192 # Check if there is clearly no need to truncate
3193 if ( $length <= 0 ) {
3194 return $ellipsis; // no text shown, nothing to format (convention)
3195 } elseif ( strlen( $text ) <= $length ) {
3196 return $text; // string short enough even *with* HTML (short-circuit)
3199 $dispLen = 0; // innerHTML legth so far
3200 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3201 $tagType = 0; // 0-open, 1-close
3202 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3203 $entityState = 0; // 0-not entity, 1-entity
3204 $tag = $ret = ''; // accumulated tag name, accumulated result string
3205 $openTags = array(); // open tag stack
3206 $maybeState = null; // possible truncation state
3208 $textLen = strlen( $text );
3209 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3210 for ( $pos = 0; true; ++
$pos ) {
3211 # Consider truncation once the display length has reached the maximim.
3212 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3213 # Check that we're not in the middle of a bracket/entity...
3214 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3215 if ( !$testingEllipsis ) {
3216 $testingEllipsis = true;
3217 # Save where we are; we will truncate here unless there turn out to
3218 # be so few remaining characters that truncation is not necessary.
3219 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3220 $maybeState = array( $ret, $openTags ); // save state
3222 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3223 # String in fact does need truncation, the truncation point was OK.
3224 list( $ret, $openTags ) = $maybeState; // reload state
3225 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3226 $ret .= $ellipsis; // add ellipsis
3230 if ( $pos >= $textLen ) break; // extra iteration just for above checks
3232 # Read the next char...
3234 $lastCh = $pos ?
$text[$pos - 1] : '';
3235 $ret .= $ch; // add to result string
3237 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3238 $entityState = 0; // for bad HTML
3239 $bracketState = 1; // tag started (checking for backslash)
3240 } elseif ( $ch == '>' ) {
3241 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3242 $entityState = 0; // for bad HTML
3243 $bracketState = 0; // out of brackets
3244 } elseif ( $bracketState == 1 ) {
3246 $tagType = 1; // close tag (e.g. "</span>")
3248 $tagType = 0; // open tag (e.g. "<span>")
3251 $bracketState = 2; // building tag name
3252 } elseif ( $bracketState == 2 ) {
3256 // Name found (e.g. "<a href=..."), add on tag attributes...
3257 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3259 } elseif ( $bracketState == 0 ) {
3260 if ( $entityState ) {
3263 $dispLen++
; // entity is one displayed char
3266 if ( $neLength == 0 && !$maybeState ) {
3267 // Save state without $ch. We want to *hit* the first
3268 // display char (to get tags) but not *use* it if truncating.
3269 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3272 $entityState = 1; // entity found, (e.g. " ")
3274 $dispLen++
; // this char is displayed
3275 // Add the next $max display text chars after this in one swoop...
3276 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3277 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3278 $dispLen +
= $skipped;
3284 // Close the last tag if left unclosed by bad HTML
3285 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3286 while ( count( $openTags ) > 0 ) {
3287 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3293 * truncateHtml() helper function
3294 * like strcspn() but adds the skipped chars to $ret
3303 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3304 if ( $len === null ) {
3305 $len = -1; // -1 means "no limit" for strcspn
3306 } elseif ( $len < 0 ) {
3310 if ( $start < strlen( $text ) ) {
3311 $skipCount = strcspn( $text, $search, $start, $len );
3312 $ret .= substr( $text, $start, $skipCount );
3318 * truncateHtml() helper function
3319 * (a) push or pop $tag from $openTags as needed
3320 * (b) clear $tag value
3321 * @param &$tag string Current HTML tag name we are looking at
3322 * @param $tagType int (0-open tag, 1-close tag)
3323 * @param $lastCh string Character before the '>' that ended this tag
3324 * @param &$openTags array Open tag stack (not accounting for $tag)
3326 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3327 $tag = ltrim( $tag );
3329 if ( $tagType == 0 && $lastCh != '/' ) {
3330 $openTags[] = $tag; // tag opened (didn't close itself)
3331 } elseif ( $tagType == 1 ) {
3332 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3333 array_pop( $openTags ); // tag closed
3341 * Grammatical transformations, needed for inflected languages
3342 * Invoked by putting {{grammar:case|word}} in a message
3344 * @param $word string
3345 * @param $case string
3348 function convertGrammar( $word, $case ) {
3349 global $wgGrammarForms;
3350 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3351 return $wgGrammarForms[$this->getCode()][$case][$word];
3356 * Get the grammar forms for the content language
3357 * @return array of grammar forms
3360 function getGrammarForms() {
3361 global $wgGrammarForms;
3362 if ( isset( $wgGrammarForms[$this->getCode()] ) && is_array( $wgGrammarForms[$this->getCode()] ) ) {
3363 return $wgGrammarForms[$this->getCode()];
3368 * Provides an alternative text depending on specified gender.
3369 * Usage {{gender:username|masculine|feminine|neutral}}.
3370 * username is optional, in which case the gender of current user is used,
3371 * but only in (some) interface messages; otherwise default gender is used.
3373 * If no forms are given, an empty string is returned. If only one form is
3374 * given, it will be returned unconditionally. These details are implied by
3375 * the caller and cannot be overridden in subclasses.
3377 * If more than one form is given, the default is to use the neutral one
3378 * if it is specified, and to use the masculine one otherwise. These
3379 * details can be overridden in subclasses.
3381 * @param $gender string
3382 * @param $forms array
3386 function gender( $gender, $forms ) {
3387 if ( !count( $forms ) ) {
3390 $forms = $this->preConvertPlural( $forms, 2 );
3391 if ( $gender === 'male' ) {
3394 if ( $gender === 'female' ) {
3397 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3401 * Plural form transformations, needed for some languages.
3402 * For example, there are 3 form of plural in Russian and Polish,
3403 * depending on "count mod 10". See [[w:Plural]]
3404 * For English it is pretty simple.
3406 * Invoked by putting {{plural:count|wordform1|wordform2}}
3407 * or {{plural:count|wordform1|wordform2|wordform3}}
3409 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3411 * @param $count Integer: non-localized number
3412 * @param $forms Array: different plural forms
3413 * @return string Correct form of plural for $count in this language
3415 function convertPlural( $count, $forms ) {
3416 if ( !count( $forms ) ) {
3419 $forms = $this->preConvertPlural( $forms, 2 );
3421 return ( $count == 1 ) ?
$forms[0] : $forms[1];
3425 * Checks that convertPlural was given an array and pads it to requested
3426 * amount of forms by copying the last one.
3428 * @param $count Integer: How many forms should there be at least
3429 * @param $forms Array of forms given to convertPlural
3430 * @return array Padded array of forms or an exception if not an array
3432 protected function preConvertPlural( /* Array */ $forms, $count ) {
3433 while ( count( $forms ) < $count ) {
3434 $forms[] = $forms[count( $forms ) - 1];
3440 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
3441 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3442 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3443 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3446 * @param $str String: the validated block duration in English
3447 * @return string Somehow translated block duration
3448 * @see LanguageFi.php for example implementation
3450 function translateBlockExpiry( $str ) {
3451 $duration = SpecialBlock
::getSuggestedDurations( $this );
3452 foreach ( $duration as $show => $value ) {
3453 if ( strcmp( $str, $value ) == 0 ) {
3454 return htmlspecialchars( trim( $show ) );
3458 // Since usually only infinite or indefinite is only on list, so try
3459 // equivalents if still here.
3460 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3461 if ( in_array( $str, $indefs ) ) {
3462 foreach ( $indefs as $val ) {
3463 $show = array_search( $val, $duration, true );
3464 if ( $show !== false ) {
3465 return htmlspecialchars( trim( $show ) );
3469 // If all else fails, return the original string.
3474 * languages like Chinese need to be segmented in order for the diff
3477 * @param $text String
3480 public function segmentForDiff( $text ) {
3485 * and unsegment to show the result
3487 * @param $text String
3490 public function unsegmentForDiff( $text ) {
3495 * Return the LanguageConverter used in the Language
3498 * @return LanguageConverter
3500 public function getConverter() {
3501 return $this->mConverter
;
3505 * convert text to all supported variants
3507 * @param $text string
3510 public function autoConvertToAllVariants( $text ) {
3511 return $this->mConverter
->autoConvertToAllVariants( $text );
3515 * convert text to different variants of a language.
3517 * @param $text string
3520 public function convert( $text ) {
3521 return $this->mConverter
->convert( $text );
3525 * Convert a Title object to a string in the preferred variant
3527 * @param $title Title
3530 public function convertTitle( $title ) {
3531 return $this->mConverter
->convertTitle( $title );
3535 * Check if this is a language with variants
3539 public function hasVariants() {
3540 return sizeof( $this->getVariants() ) > 1;
3544 * Check if the language has the specific variant
3547 * @param $variant string
3550 public function hasVariant( $variant ) {
3551 return (bool)$this->mConverter
->validateVariant( $variant );
3555 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3557 * @param $text string
3560 public function armourMath( $text ) {
3561 return $this->mConverter
->armourMath( $text );
3565 * Perform output conversion on a string, and encode for safe HTML output.
3566 * @param $text String text to be converted
3567 * @param $isTitle Bool whether this conversion is for the article title
3569 * @todo this should get integrated somewhere sane
3571 public function convertHtml( $text, $isTitle = false ) {
3572 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3576 * @param $key string
3579 public function convertCategoryKey( $key ) {
3580 return $this->mConverter
->convertCategoryKey( $key );
3584 * Get the list of variants supported by this language
3585 * see sample implementation in LanguageZh.php
3587 * @return array an array of language codes
3589 public function getVariants() {
3590 return $this->mConverter
->getVariants();
3596 public function getPreferredVariant() {
3597 return $this->mConverter
->getPreferredVariant();
3603 public function getDefaultVariant() {
3604 return $this->mConverter
->getDefaultVariant();
3610 public function getURLVariant() {
3611 return $this->mConverter
->getURLVariant();
3615 * If a language supports multiple variants, it is
3616 * possible that non-existing link in one variant
3617 * actually exists in another variant. this function
3618 * tries to find it. See e.g. LanguageZh.php
3620 * @param $link String: the name of the link
3621 * @param $nt Mixed: the title object of the link
3622 * @param $ignoreOtherCond Boolean: to disable other conditions when
3623 * we need to transclude a template or update a category's link
3624 * @return null the input parameters may be modified upon return
3626 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3627 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
3631 * If a language supports multiple variants, converts text
3632 * into an array of all possible variants of the text:
3633 * 'variant' => text in that variant
3635 * @deprecated since 1.17 Use autoConvertToAllVariants()
3637 * @param $text string
3641 public function convertLinkToAllVariants( $text ) {
3642 return $this->mConverter
->convertLinkToAllVariants( $text );
3646 * returns language specific options used by User::getPageRenderHash()
3647 * for example, the preferred language variant
3651 function getExtraHashOptions() {
3652 return $this->mConverter
->getExtraHashOptions();
3656 * For languages that support multiple variants, the title of an
3657 * article may be displayed differently in different variants. this
3658 * function returns the apporiate title defined in the body of the article.
3662 public function getParsedTitle() {
3663 return $this->mConverter
->getParsedTitle();
3667 * Enclose a string with the "no conversion" tag. This is used by
3668 * various functions in the Parser
3670 * @param $text String: text to be tagged for no conversion
3671 * @param $noParse bool
3672 * @return string the tagged text
3674 public function markNoConversion( $text, $noParse = false ) {
3675 return $this->mConverter
->markNoConversion( $text, $noParse );
3679 * A regular expression to match legal word-trailing characters
3680 * which should be merged onto a link of the form [[foo]]bar.
3684 public function linkTrail() {
3685 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
3691 function getLangObj() {
3696 * Get the RFC 3066 code for this language object
3698 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3699 * htmlspecialchars() or similar
3703 public function getCode() {
3704 return $this->mCode
;
3708 * Get the code in Bcp47 format which we can use
3709 * inside of html lang="" tags.
3711 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
3712 * htmlspecialchars() or similar.
3717 public function getHtmlCode() {
3718 if ( is_null( $this->mHtmlCode
) ) {
3719 $this->mHtmlCode
= wfBCP47( $this->getCode() );
3721 return $this->mHtmlCode
;
3725 * @param $code string
3727 public function setCode( $code ) {
3728 $this->mCode
= $code;
3729 // Ensure we don't leave an incorrect html code lying around
3730 $this->mHtmlCode
= null;
3734 * Get the name of a file for a certain language code
3735 * @param $prefix string Prepend this to the filename
3736 * @param $code string Language code
3737 * @param $suffix string Append this to the filename
3738 * @throws MWException
3739 * @return string $prefix . $mangledCode . $suffix
3741 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3742 // Protect against path traversal
3743 if ( !Language
::isValidCode( $code )
3744 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3746 throw new MWException( "Invalid language code \"$code\"" );
3749 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3753 * Get the language code from a file name. Inverse of getFileName()
3754 * @param $filename string $prefix . $languageCode . $suffix
3755 * @param $prefix string Prefix before the language code
3756 * @param $suffix string Suffix after the language code
3757 * @return string Language code, or false if $prefix or $suffix isn't found
3759 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3761 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3762 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3763 if ( !count( $m ) ) {
3766 return str_replace( '_', '-', strtolower( $m[1] ) );
3770 * @param $code string
3773 public static function getMessagesFileName( $code ) {
3775 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3776 wfRunHooks( 'Language::getMessagesFileName', array( $code, &$file ) );
3781 * @param $code string
3784 public static function getClassFileName( $code ) {
3786 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3790 * Get the first fallback for a given language.
3792 * @param $code string
3794 * @return bool|string
3796 public static function getFallbackFor( $code ) {
3797 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3800 $fallbacks = self
::getFallbacksFor( $code );
3801 $first = array_shift( $fallbacks );
3807 * Get the ordered list of fallback languages.
3810 * @param $code string Language code
3813 public static function getFallbacksFor( $code ) {
3814 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
3817 $v = self
::getLocalisationCache()->getItem( $code, 'fallback' );
3818 $v = array_map( 'trim', explode( ',', $v ) );
3819 if ( $v[count( $v ) - 1] !== 'en' ) {
3827 * Get all messages for a given language
3828 * WARNING: this may take a long time. If you just need all message *keys*
3829 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
3831 * @param $code string
3835 public static function getMessagesFor( $code ) {
3836 return self
::getLocalisationCache()->getItem( $code, 'messages' );
3840 * Get a message for a given language
3842 * @param $key string
3843 * @param $code string
3847 public static function getMessageFor( $key, $code ) {
3848 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
3852 * Get all message keys for a given language. This is a faster alternative to
3853 * array_keys( Language::getMessagesFor( $code ) )
3856 * @param $code string Language code
3857 * @return array of message keys (strings)
3859 public static function getMessageKeysFor( $code ) {
3860 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
3867 function fixVariableInNamespace( $talk ) {
3868 if ( strpos( $talk, '$1' ) === false ) {
3872 global $wgMetaNamespace;
3873 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
3875 # Allow grammar transformations
3876 # Allowing full message-style parsing would make simple requests
3877 # such as action=raw much more expensive than they need to be.
3878 # This will hopefully cover most cases.
3879 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
3880 array( &$this, 'replaceGrammarInNamespace' ), $talk );
3881 return str_replace( ' ', '_', $talk );
3888 function replaceGrammarInNamespace( $m ) {
3889 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
3893 * @throws MWException
3896 static function getCaseMaps() {
3897 static $wikiUpperChars, $wikiLowerChars;
3898 if ( isset( $wikiUpperChars ) ) {
3899 return array( $wikiUpperChars, $wikiLowerChars );
3902 wfProfileIn( __METHOD__
);
3903 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
3904 if ( $arr === false ) {
3905 throw new MWException(
3906 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
3908 $wikiUpperChars = $arr['wikiUpperChars'];
3909 $wikiLowerChars = $arr['wikiLowerChars'];
3910 wfProfileOut( __METHOD__
);
3911 return array( $wikiUpperChars, $wikiLowerChars );
3915 * Decode an expiry (block, protection, etc) which has come from the DB
3917 * @FIXME: why are we returnings DBMS-dependent strings???
3919 * @param $expiry String: Database expiry String
3920 * @param $format Bool|Int true to process using language functions, or TS_ constant
3921 * to return the expiry in a given timestamp
3924 public function formatExpiry( $expiry, $format = true ) {
3925 static $infinity, $infinityMsg;
3926 if ( $infinity === null ) {
3927 $infinityMsg = wfMessage( 'infiniteblock' );
3928 $infinity = wfGetDB( DB_SLAVE
)->getInfinity();
3931 if ( $expiry == '' ||
$expiry == $infinity ) {
3932 return $format === true
3936 return $format === true
3937 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
3938 : wfTimestamp( $format, $expiry );
3944 * @param $seconds int|float
3945 * @param $format Array Optional
3946 * If $format['avoid'] == 'avoidseconds' - don't mention seconds if $seconds >= 1 hour
3947 * If $format['avoid'] == 'avoidminutes' - don't mention seconds/minutes if $seconds > 48 hours
3948 * If $format['noabbrevs'] is true - use 'seconds' and friends instead of 'seconds-abbrev' and friends
3949 * For backwards compatibility, $format may also be one of the strings 'avoidseconds' or 'avoidminutes'
3952 function formatTimePeriod( $seconds, $format = array() ) {
3953 if ( !is_array( $format ) ) {
3954 $format = array( 'avoid' => $format ); // For backwards compatibility
3956 if ( !isset( $format['avoid'] ) ) {
3957 $format['avoid'] = false;
3959 if ( !isset( $format['noabbrevs' ] ) ) {
3960 $format['noabbrevs'] = false;
3962 $secondsMsg = wfMessage(
3963 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
3964 $minutesMsg = wfMessage(
3965 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
3966 $hoursMsg = wfMessage(
3967 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
3968 $daysMsg = wfMessage(
3969 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
3971 if ( round( $seconds * 10 ) < 100 ) {
3972 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
3973 $s = $secondsMsg->params( $s )->text();
3974 } elseif ( round( $seconds ) < 60 ) {
3975 $s = $this->formatNum( round( $seconds ) );
3976 $s = $secondsMsg->params( $s )->text();
3977 } elseif ( round( $seconds ) < 3600 ) {
3978 $minutes = floor( $seconds / 60 );
3979 $secondsPart = round( fmod( $seconds, 60 ) );
3980 if ( $secondsPart == 60 ) {
3984 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
3986 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
3987 } elseif ( round( $seconds ) <= 2 * 86400 ) {
3988 $hours = floor( $seconds / 3600 );
3989 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
3990 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
3991 if ( $secondsPart == 60 ) {
3995 if ( $minutes == 60 ) {
3999 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4001 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4002 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4003 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4006 $days = floor( $seconds / 86400 );
4007 if ( $format['avoid'] === 'avoidminutes' ) {
4008 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4009 if ( $hours == 24 ) {
4013 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4015 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4016 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4017 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4018 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4019 if ( $minutes == 60 ) {
4023 if ( $hours == 24 ) {
4027 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4029 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4031 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4033 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4035 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4042 * Format a bitrate for output, using an appropriate
4043 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question
4045 * This use base 1000. For base 1024 use formatSize(), for another base
4046 * see formatComputingNumbers()
4051 function formatBitrate( $bps ) {
4052 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4056 * @param $size int Size of the unit
4057 * @param $boundary int Size boundary (1000, or 1024 in most cases)
4058 * @param $messageKey string Message key to be uesd
4061 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4063 return str_replace( '$1', $this->formatNum( $size ),
4064 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4067 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4070 $maxIndex = count( $sizes ) - 1;
4071 while ( $size >= $boundary && $index < $maxIndex ) {
4076 // For small sizes no decimal places necessary
4079 // For MB and bigger two decimal places are smarter
4082 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4084 $size = round( $size, $round );
4085 $text = $this->getMessageFromDB( $msg );
4086 return str_replace( '$1', $this->formatNum( $size ), $text );
4090 * Format a size in bytes for output, using an appropriate
4091 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4093 * This method use base 1024. For base 1000 use formatBitrate(), for
4094 * another base see formatComputingNumbers()
4096 * @param $size int Size to format
4097 * @return string Plain text (not HTML)
4099 function formatSize( $size ) {
4100 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4104 * Make a list item, used by various special pages
4106 * @param $page String Page link
4107 * @param $details String Text between brackets
4108 * @param $oppositedm Boolean Add the direction mark opposite to your
4109 * language, to display text properly
4112 function specialList( $page, $details, $oppositedm = true ) {
4113 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) .
4114 $this->getDirMark();
4115 $details = $details ?
$dirmark . $this->getMessageFromDB( 'word-separator' ) .
4116 wfMsgExt( 'parentheses', array( 'escape', 'replaceafter', 'language' => $this ), $details ) : '';
4117 return $page . $details;
4121 * Generate (prev x| next x) (20|50|100...) type links for paging
4123 * @param $title Title object to link
4124 * @param $offset Integer offset parameter
4125 * @param $limit Integer limit parameter
4126 * @param $query array|String optional URL query parameter string
4127 * @param $atend Bool optional param for specified if this is the last page
4130 public function viewPrevNext( Title
$title, $offset, $limit, array $query = array(), $atend = false ) {
4131 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4133 # Make 'previous' link
4134 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4135 if ( $offset > 0 ) {
4136 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4137 $query, $prev, 'prevn-title', 'mw-prevlink' );
4139 $plink = htmlspecialchars( $prev );
4143 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4145 $nlink = htmlspecialchars( $next );
4147 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4148 $query, $next, 'prevn-title', 'mw-nextlink' );
4151 # Make links to set number of items per page
4152 $numLinks = array();
4153 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4154 $numLinks[] = $this->numLink( $title, $offset, $num,
4155 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4158 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4159 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4163 * Helper function for viewPrevNext() that generates links
4165 * @param $title Title object to link
4166 * @param $offset Integer offset parameter
4167 * @param $limit Integer limit parameter
4168 * @param $query Array extra query parameters
4169 * @param $link String text to use for the link; will be escaped
4170 * @param $tooltipMsg String name of the message to use as tooltip
4171 * @param $class String value of the "class" attribute of the link
4172 * @return String HTML fragment
4174 private function numLink( Title
$title, $offset, $limit, array $query, $link, $tooltipMsg, $class ) {
4175 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4176 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4177 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4178 'title' => $tooltip, 'class' => $class ), $link );
4182 * Get the conversion rule title, if any.
4186 public function getConvRuleTitle() {
4187 return $this->mConverter
->getConvRuleTitle();